<?php
/**
 * 文件对象
 * 
 * 封装了文件系统的删除、新建、写入、权限设置等操作
 * 
 * 本类中的权限操作仅限于linux/unix
 *
 * @version $Id$
 * @package if
 * @subpackage util.io
 */
class IFile extends IObject implements IIterable {
	private $filename;
	/**
	 * 用户权限
	 *
	 */
	const PER_USER = 1;
	
	/**
	 * 用户所在组权限
	 *
	 */
	const PER_GROUP = 2;
	
	/**
	 * 其他组权限
	 *
	 */
	const PER_OTHER = 4;
	
	/**
	 * 所有组权限
	 *
	 */
	const PER_ALL = 8;
	
	/**
	 * 文章列表
	 *
	 * @var array
	 */
	private $files;
	
	
	/**
	 * 构造器
	 *
	 * @param string $filename 文件名
	 * @since 1.0
	 */
	function __construct($filename) {
		$this->filename = $filename;
	}
	
	/**
	 * 判断是否可以执行
	 *
	 * @return boolean
	 * @since 1.0
	 */
	function canExecute() {
		return is_executable($this->filename);
	}
	
	/**
	 * 判断该对象是否可读
	 *
	 * @return boolean
	 * @since 1.0
	 */	
	function canRead() {
		return is_readable($this->filename);
	}
	
	/**
	 * 判断该对象是否可写
	 *
	 * @return boolean
	 * @since 1.0
	 */
	function canWrite() {
		return is_writeable($this->filename);
	}
	
	/**
	 * 创建文件
	 *
	 * 从1.1.2起，I-F会自动尝试创建该文件上的多级目录
	 * 
	 * @return boolean
	 * @since 1.0
	 */
	function create() {
		if (!$this->isFile()) {
			$writer = new IFileWriter($this);
			$writer->write("", true);
			$writer->close();
			return is_file($this->filename);
		}
		return true;
	}
	
	/**
	 * 清空当前文件或目录
	 * 
	 * 如果是文件，则清空文件内容；如果是目录，则删除该目录下的所有子文件和子目录
	 *
	 * @return boolean
	 * @since 1.0
	 * @see delete
	 */
	function clear() {
		if ($this->isFile()) {
			$fp = fopen($this->filename, "w+");
			fclose($fp);
			return (bool)$fp;
		}
		else if ($this->isDir()) {
			foreach ($this->lists() as $file) {
				$file->delete(true);
			}
		}
		return true;
	}
	
	/**
	 * 删除当前文件
	 * 
	 * 如果要删除的非空目录，需指定$recursively为true才能删除
	 * 
	 * @param boolean $recursively 是否递归地删除
	 * @return boolean
	 * @since 1.0
	 * @see clear
	 */
	function delete($recursively = true) {
		if ($this->isFile()) {
			return unlink($this->filename);
		}
		else if ($this->isDir()) {
			if ($recursively) {
				foreach ($this->lists() as $file) {
					$file->delete(true);
				}
			}
			return rmdir($this->filename);
		}
		return false;
	}
	
	/**
	 * 检查文件/目录是否存在
	 *
	 * @return boolean
	 * @since 1.0
	 */
	function exists() {
		return file_exists($this->filename);
	}
	
	/**
	 * 取得绝对路径的文件对象
	 *
	 * @return IFile
	 * @since 1.0
	 */
	function absFile() {
		return new IFile($this->absPath());
	}
	
	/**
	 * 得到绝对路径
	 * 
	 * 如果文件不存在，则返回null
	 *
	 * @return string|null
	 * @since 1.0
	 */
	function absPath() {
		if ($this->exists()) {
			return realpath($this->path());
		}
		return null;
	}
	
	/**
	 * 取得权限组
	 * 
	 * 结果为：
	 * 
	 * array(IFile::PER_USER, IFile::PER_GROUP, IFile::PER_OTHER, IFile::PER_ALL)
	 *
	 * @return array
	 * @since 1.0
	 */
	function permissions() {
		return array(self::PER_USER, self::PER_GROUP, self::PER_OTHER, self::PER_ALL);
	}
	
	/**
	 * 得到文件名
	 *
	 * @return string
	 * @since 1.0
	 */
	function name() {
		return basename($this->filename);
	}
	
	/**
	 * 得到上级目录路径
	 * 
	 * 如果文件不存在，也返回其所在的目录
	 * 
	 * @return string
	 * @since 1.0
	 */
	function parent() {
		return dirname($this->filename);
	}
	
	/**
	 * 得到上级文件对象
	 *
	 * @return IFile
	 * @since 1.0
	 */
	function parentFile() {
		return new IFile($this->parent());
	}
	
	/**
	 * 取得当前对象的路径
	 *
	 * @param boolean $original 是否显示处理前的文件路径
	 * @return string
	 * @since 1.0
	 */
	function path($original = false) {
		return $original ? $this->filename : preg_replace("/[\\/\\\\]+/", self::separator(), $this->filename);
	}
	
	/**
	 * 判断当前对象的路径是否为绝对路径
	 *
	 * @return boolean
	 * @since 1.0
	 */
	function isAbs() {
		$path = $this->path();
		if (self::isAbsPath($path)) {
			return true;
		}
		return false;
	}
	
	/**
	 * 判断是否是绝对路径
	 *
	 * @param string $path 路径
	 * @return boolean
	 * @since 1.0
	 */
	static function isAbsPath($path) {
		return if_is_absolute_path($path);
	}
	
	/**
	 * 检查当前路径是否为目录
	 *
	 * @return boolean
	 * @since 1.0
	 */
	function isDir() {
		return is_dir($this->filename);
	}
	
	/**
	 * 检查当前路径是否为文件
	 *
	 * @return boolean
	 * @since 1.0
	 */
	function isFile() {
		return is_file($this->filename);
	}
	
	/**
	 * 取得最后修改时间
	 *
	 * @return integer
	 * @since 1.0
	 */
	function lastModified() {
		if ($this->exists()) {
			return filemtime($this->filename);
		}
		return null;
	}
	
	/**
	 * 取得文件尺寸
	 *
	 * @return integer
	 * @since 1.0
	 */
	function length() {
		return filesize($this->filename);
	}
	
	/**
	 * 取得当前目录下的文件列表
	 *
	 * 返回一个包含每一个子文件对象的数组，并排除了.和..两个路径。
	 * 
	 * 从1.0开始加入$recursively参数，可以递归地查找一个目录下所有的子文件对象，包括目录。
	 *
	 * @param string|null $filter 过滤规则，比如*.php
	 * @param boolean $recursively 是否递归查找
	 * @param boolean $skipHidden 是否忽略（.*）隐藏文件，如.svn, .htaccess，v1.1.1加入
	 * @return array
	 * @since 1.0
	 */
	function lists($filter = null, $recursively = false, $skipHidden = false) {
		if ($this->isDir()) {
			if (!$filter) {
				$files = scandir($this->absPath());
			} else {
				$files = glob($this->absPath() . self::separator() . $filter, GLOB_BRACE);
			}
			$list = array();
			$found = array();
			foreach ($files as $file) {
				if ($file != "." && $file != "..") {
					if ($skipHidden && preg_match("/(^|[\/\\\\])\..+/", $file)) {
						continue;
					}
					$real = self::isAbsPath($file) ? $file : realpath($this->absPath() . self::separator() . $file);
					$found[] = $real;
					
					$childFile = new IFile($real);
					array_push($list, $childFile);
					
					//递归地加入子文件对象
					if ($recursively && $childFile->isDir()) {
						foreach ($childFile->lists($filter, true, $skipHidden) as $newChildFile) {
							array_push($list, $newChildFile);
						}
					}
				}
			}
			
			//尝试查找目录不匹配但是文件名匹配的项
			if ($filter && $recursively) {
				$files = scandir($this->absPath());
				foreach ($files as $file) {
					if ($file != "." && $file != "..") {
						if ($skipHidden && preg_match("/(^|[\/\\\\])\..+/", $file)) {
							continue;
						}
						$real = self::isAbsPath($file) ? $file : realpath($this->absPath() . self::separator() . $file);
						if (!in_array($real, $found)) {						
							//递归地加入子文件对象
							$childFile = new IFile($real);
							if ($recursively && $childFile->isDir()) {
								foreach ($childFile->lists($filter, true, $skipHidden) as $newChildFile) {
									array_push($list, $newChildFile);
								}
							}
						}
					}
				}
			}

			return $list;
		}
		return array();
	}
	
	/**
	 * 创建目录
	 *
	 * @param integer $mode 模式，使用八进制表示
	 * @return boolean
	 * @since 1.0
	 */
	function mkdir($mode = 0700) {
		if (!$this->exists()) {
			return mkdir($this->filename, $mode);
		}
		return true;
	}
	
	/**
	 * 创建目录
	 *
	 * 与mkdir不同，如果要创建的目录上级不存在，将自动创建
	 * 
	 * @param integer $mode 模式，使用八进制表示
	 * @return string
	 * @see mkdir
	 * @since 1.0
	 */
	function mkdirs($mode = 0700) {
		if (!$this->exists()) {
			return mkdir($this->filename, $mode, true);
		}
		return true;
	}
	
	/**
	 * 改名为新的文件对象
	 *
	 * @param IFile $dest 目标文件对象
	 * @since 1.0
	 */
	function renameTo(IFile $dest) {
		if (rename($this->filename, $dest->path())) {
			$this->filename = $dest->path();
		}
	}
	
	/**
	 * 取得文件模式（八进制）
	 *
	 * @return string
	 * @since 1.0
	 */
	function mode() {
		if ($this->exists()) {
			$perms = fileperms($this->filename);
			$oct = sprintf("%o", $perms);
			return substr($oct, -4);
		}
		return "0000";
	}
	
	######################### 以下方法用于支持模式的计算 ####################
	private function _octToBinary($mode) {
		return sprintf("%016s", base_convert($mode, 8, 2));
	}
	
	private function _plusMode($newMode) {
		return bindec($this->_octToBinary($this->mode()) | $this->_octToBinary($newMode));
	}
	
	private function _minusMode($newMode) {
		$a = $this->_octToBinary($this->mode());
		$b = $this->_octToBinary($newMode);
		return bindec($a) ^ bindec($a & $b);
		//return bindec($a) & (~bindec($b));
	}
	
	private function _modeByGroup($mode, $group, $number) {
		foreach ($this->permissions() as $permissionGroup) {
			if ($permissionGroup & $group) {
				switch($permissionGroup) {
					case self::PER_USER:
						$mode = substr($mode, 0, 1) . $number . substr($mode, 2);
						break;
					case self::PER_GROUP:
						$mode = substr($mode, 0, 2) . $number . substr($mode, 3);
						break;
					case self::PER_OTHER:
						$mode = substr($mode, 0, 3) . $number;
						break;
					case self::PER_ALL:
						$mode = "0" . str_repeat($number, 3);
						break;
				}
			}
		}
		return $mode;
	}
	######################### 模式计算辅助方法结束 ####################
	
	/**
	 * 设置是否可执行
	 *
	 * @param boolean $executable 是否可执行
	 * @param integer $group 应用到的权限组
	 * @return boolean
	 * @since 1.0
	 */
	function setExecutable($executable, $group = IFile::PER_ALL) {
		if ($this->exists()) {
			$newMode = "0000";
			$diffMode = $this->_modeByGroup("0000", $group, "1");
			if ($executable) {
				$newMode = $this->_plusMode($diffMode);
			}
			else {
				$newMode = $this->_minusMode($diffMode);
			}
			return chmod($this->filename, $newMode);
		}
		return false;
	}
	
	/**
	 * 设置文件最后修改时间
	 *
	 * @param integer $time 时间戳
	 * @return boolean
	 * @since 1.0
	 */
	function setLastModified($time) {
		if ($this->exists()) {
			return touch($this->filename, $time);
		}
		return false;
	}
	
	/**
	 * 设置文件是否可读
	 *
	 * @param boolean $readable 是否可读
	 * @param integer $group 应用到的权限组
	 * @return boolean
	 * @since 1.0
	 */
	function setReadable($readable, $group = IFile::PER_ALL) {
		if ($this->exists()) {
			$newMode = 0;
			$diffMode = $this->_modeByGroup("0000", $group, "4");
			if ($readable) {
				$newMode = $this->_plusMode($diffMode);
			}
			else {
				$newMode = $this->_minusMode($diffMode);
			}
			return chmod($this->filename, $newMode);
		}
		return false;
	}
	
	/**
	 * 设置文件只可以读
	 *
	 * @return boolean
	 * @since 1.0
	 */
	function setReadOnly() {
		if ($this->exists()) {
			return chmod($this->filename, 0444);
		}
		return false;
	}

	/**
	 * 设定文件是否可写
	 *
	 * @param boolean $writable 是否可写
	 * @param integer $group 应用到的权限组
	 * @return boolean
	 * @since 1.0
	 */
	function setWritable($writable, $group = IFile::PER_ALL) {
		if ($this->exists()) {
			$newMode = 0;
			$diffMode = $this->_modeByGroup("0000", $group, "2");
			if ($writable) {
				$newMode = $this->_plusMode($diffMode);
			}
			else {
				$newMode = $this->_minusMode($diffMode);
			}
			return chmod($this->filename, $newMode);
		}
		return false;
	}
	
	/**
	 * 返回文件的类型。可能的值有 fifo，char，dir，block，link，file 和 unknown。 
	 *
	 * @return string
	 * @since 1.0
	 */
	function type() {
		if ($this->exists()) {
			return filetype($this->filename);
		}
		return null;
	}
	
	/**
	 * 取得目录分隔符
	 *
	 * unix/linux为/，windows为\
	 * 
	 * @return string
	 * @since 1.0
	 */
	static function separator() {
		return DIRECTORY_SEPARATOR;
	}
	
	/**
	 * 清除文件缓存
	 * 
	 * @since 1.0
	 */
	static function clearCache() {
		clearstatcache();
	}
	
	/**
	 * 取得文件读对象
	 * 
	 * 可以使用它读取文件的内容
	 *
	 * @return IFileReader
	 * @since 1.0
	 */
	function reader() {
		return new IFileReader($this);
	}
	
	/**
	 * 取得文件写入器对象
	 * 
	 * 如果文件不存在，则创建新的文件；否则会先清空原来的文件内容
	 * 
	 * 注意每次调用该方法都会产生一个新的写入器
	 *
	 * @return IFileWriter
	 * @since 1.0
	 */
	function writer() {
		return new IFileWriter($this);
	}
	
	/**
	 * 读取文件附加对象
	 * 
	 * 在文件末尾处附加新的内容
	 *
	 * @return IFileAppender
	 * @since 1.0
	 */
	function appender() {
		return new IFileAppender($this);
	}
	
	/**
	 * 取得文件扩展名
	 * 
	 * 不含小数点"."符号
	 *
	 * @param boolean $tolower 是否转换为小写
	 * @since 1.0
	 * @return string
	 */
	function ext($tolower = false) {
		$ext = pathinfo($this->filename, PATHINFO_EXTENSION);
		return $tolower?strtolower($ext):$ext;
	}
	
	/**
	 * 对文件进行md5计算
	 * 
	 * 如果文件不存在，则返回null
	 *
	 * @return string|null
	 * @since 1.0
	 */
	function md5() {
		if ($this->exists() && $this->isFile()) {
			return md5_file($this->filename);
		}
		return null;
	}
	
	/**
	 * 当前目录下的文件列表
	 *
	 * @return array
	 * @since 1.0.1
	 */
	function asArray() {
		if (is_null($this->files)) {
			$this->refresh();
		}
		return $this->files;
	}
	
	/**
	 * 当前目录下的文件列表的迭代器
	 *
	 * @return Iterator
	 * @since 1.0.1
	 */
	function getIterator() {
		$files = $this->asArray();
		return new IListIterator($files);
	}
	
	/**
	 * 刷新当前目录下的文件列表
	 *
	 * @since 1.0.1
	 */
	function refresh() {
		$this->files = $this->lists();
	}
	
	/**
	 * 判断一个偏移量是否存在
	 *
	 * 实现了 ArrayAccess 接口对应方法
	 * 
	 * @param integer $index
	 * @return boolean
	 * @since 1.0
	 */
	function offsetExists($index) {
		$this->asArray();
		return isset($this->files[$index]);
	}
	
	/**
	 * 从一个偏移量中取得数据
	 * 
	 * 实现了 ArrayAccess 接口对应方法
	 *
	 * @param integer $index 偏移量
	 * @return mixed
	 * @since 1.0
	 */
	function offsetGet($index) {
		$this->asArray();
		return isset($this->files[$index]) ? $this->files[$index] : null;
	}
	
	/**
	 * 设置偏移量位置上的值
	 *
	 * 实现了 ArrayAccess 接口对应方法
	 * 
	 * @param integer $index 偏移量
	 * @param mixed $item 值
	 * @since 1.0
	 */
	function offsetSet($index, $item) {
		$this->asArray();
		$this->files[$index] = $item;
	}
	
	/**
	 * 删除偏移量位置对应的元素
	 * 
	 * 实现了 ArrayAccess 接口对应方法
	 *
	 * @param integer $index 偏移量
	 * @since 1.0
	 */
	function offsetUnset($index) {
		$this->asArray();
		if ($this->offsetExists($index)) {
			unset($this->files[$index]);
		}
	}	
	
	/**
	 * 判断当前文件或目录是否为空
	 * 
	 * @return boolean
	 * @since 1.0.1
	 */
	function isEmpty() {
		if ($this->isDir()) {
			return count($this->asArray()) == 0;
		}
		else if ($this->isFile()) {
			$reader = $this->reader();
			$data = $reader->read();
			$reader->close();
			return $data == "";
		}
		return false;
	}
	
	/**
	 * 拷贝当前文件或目录到某个路径
	 * 
	 * v1.1.0支持copy一个目录
	 * 
	 * 如果当前对象是文件，但目标是一个目录，则将文件拷贝到目录下。如果两者都是目录，则用当前对象下的文件和目录覆盖目标下的文件和目录。
	 * 
	 * @param string|IFile $path 目标路径
	 * @param string|null $filter 过滤规则，比如*.php，v1.1.2加入
	 * @param boolean $skipHidden 是否忽略（.*）隐藏文件，如.svn, .htaccess，v1.1.2加入
	 * @return boolean
	 * @since 1.0.2
	 */
	function copyTo($path, $filter = null, $skipHidden = false) {
		if (!$this->exists()) {
			return false;
		}
		if (if_is_instance_of($path, __CLASS__)) {
			$path = $path->path(true);
		}
		if ($this->isFile()) {
			$dest = $path;
			$dir = null;
			if (is_dir($path)) {
				$dir = f($path);
				$dest = $dir->path() . "/" . $this->name();
			}
			else {
				$dir = f($path)->parentFile();
			}
			if (!$dir->exists() && !@$dir->mkdirs()) {
				throw new IOException("can not copy file from '" . $this->path() . "' to '{$path}' (cannot create folder)", 0x100108);
			}
			return copy($this->absPath(), $dest);
		}
		elseif ($this->isDir()) {
			$dir = f($path . "/" . $this->name());
			if ((!$dir->exists() || !$dir->isDir()) && !@$dir->mkdirs()) {
				throw new IOException("can not copy file from '" . $this->path() . "' to '{$path}' (cannot create folder)", 0x100109);
			}
			$ret = true;
			foreach ($this->lists($filter, false, $skipHidden) as $subfile) {
				$bool = $subfile->copyTo($dir->path(), $filter, $skipHidden);
				if (!$bool) {
					$ret = false;
				}
			}
			return $ret;
		}
		return false;
	}
	
	/**
	 * 移动当前文件或目录到某个路径
	 * 
	 * v1.1.0支持move一个目录
	 *
	 * @param string $path|IFile 目标路径
	 * @return boolean
	 * @since 1.0.2
	 */	
	function moveTo($path) {
		if ($this->copyTo($path)) {
			if ($this->delete()) {
				$this->filename = if_is_instance_of($path, __CLASS__) ? $path->path() : $path;
				return true;
			}
		}
		return false;
	}
	
	/**
	 * 创建一个临时文件
	 *
	 * @param string|null $dir 临时文件所在目录
	 * @param string $prefix 前缀
	 * @return IFile 成功返回IFile对象，否则返回null
	 * @since 1.0.2
	 */
	static function tmp($dir = null, $prefix = "if_") {
		if (!$dir) {
			$dir = IF_APP_ROOT . self::separator() . "cache";
		}
		if (!$prefix) {
			$prefix = "";
		}
		$filename = tempnam($dir, $prefix);
		return $filename ? (new IFile($filename)) : null;  
	}
}

?>